Item.php

<?php

namespace Phad;


class Item {


    /**
     * The name of your item on disk like `Blog/Page` or `Blog/List`
     */
    public string $name;

    /** a class const of Phad\Blocks that instructs the compiled item what to return */
    public $mode;

    /**
     * The base path for an item, like `/path/to/dir/Blog/Page` 
     */
    public string $path;
    /**
     * Args to pass to your compiled template. Should contain `['phad'=>$phad_instance]`
     */
    public array $args;
    /**
     * Path to the template file like `/path/to/dir/Blog/Page.php`
     */
    public string $templateFile;

    /**
     * The base dir for the phad instance that contains all other relative items
     */
    public string $dir;

    /** This item's template's source code */
    public string $source;


    /**
     * true to always compile views (but one instance still only compiles once)
     */
    public $force_compile = false;

    /**
     * Stored output so you can call `html()` multiple times without re-executing
     */
    protected $html = false;
    /**
     * Stored output so you can call `html_with_no_data()` multiple times without re-executing
     */
    protected $html_with_no_data = false;
    /**
     * @deprecated and will be removed soon
     */
    protected array $routes = [];

    /**
     * Stored output of your item info, so `info()` can be called multiple times without re-executing
     */
    protected $item_info = null;

    /** to prevent the same view object from compiling multiple times */
    protected $instanceHasBeenCompiled = false;

    /** args to pass to every item. If item-specific args are set with the same key, they will override the global args. */
    static public array $global_args = [];


    /**
     * An interface for your item templates
     *
     * @param $name the name of the item like `Taeluf/Blog` (the item's template should be in `$dir` at `Taeluf/Blog.php`)
     * @param $dir the directory items are in
     * @param $args arguments to pass to your item's template. Will be `extract`ed prior to `require`. 
     */
    public function __construct(string $name,string $dir,array $args=[]){
        $this->name = $name;
        $this->dir = $dir;
        $this->args = $args;
        $this->args = array_merge(static::$global_args, $args);
        $this->path = $this->dir.'/'.$name;
        $this->templateFile = $this->dir.'/'.$name.'.php';
    }

    /**
     * Get array of the item's data (instead of html)
     * @return array of ONLY the first item's data
     */
    public function rows(): array{
        $this->compileAsNeeded();
        // if ($this->rows !== false)return $this->rows;
        $phad_block = \Phad\Blocks::ITEM_DATA;

        $args = $this->args;
        extract($args);
        ob_start();
        $ret = require($this->get_compiled_file_path());
        $output = ob_get_clean();

        // $this->mode = null;
        // $this->html = $output;
        return $ret;
    }

    /**
     * Get array of sitemap data, used for compiling sitemaps
     */
    public function sitemap_data():array{
        $this->compileAsNeeded();
        
        $phad_block = \Phad\Blocks::SITEMAP_META;
        $args = $this->args;
        // extract($args);
        ob_start();
        $sitemapData = require($this->get_compiled_file_path());
        ob_get_clean();

        return $sitemapData;
    }
    /**
     * Get array of routes
     */
    public function routes():array{
        $this->compileAsNeeded();
        
        $phad_block = \Phad\Blocks::ROUTE_META;
        $args = $this->args;
        // $phad = $args['phad'];
        // extract($args);
        ob_start();
        $routes = require($this->get_compiled_file_path());
        ob_get_clean();

        return $routes;
    }
    /**
     * Get item info. 
     * @note If there are multiple items in one template, only returns info for the first item.
     *
     * @return array of item information
     */
    public function info(){
        $this->compileAsNeeded();
        if ($this->item_info !== null)return $this->item_info;
        
        $phad_block = \Phad\Blocks::ITEM_META;
        $args = $this->args;
        extract($args);
        ob_start();
        $itemData = require($this->get_compiled_file_path());
        ob_get_clean();

        if ($itemData==null)$itemData = (object)['type'=>'script'];
        $this->item_info = $itemData;
        return $itemData;
    }

    /**
     * Delete the item
     */
    public function delete(){
        $this->compileAsNeeded();
        // if ($this->output !== false)return $this->output;
        $phad_block = \Phad\Blocks::FORM_DELETE;

        $args = $this->args;

        // $args['phad_submit_values'] = $submitArgs;

        extract($args);
        ob_start();
        require($this->get_compiled_file_path());
        $output = ob_get_clean();

        // $this->output = $output;
        return $output;
    }
    /**
     * Submit data for the item
     */
    public function submit(){
        $this->compileAsNeeded();
        // if ($this->output !== false)return $this->output;
        $phad_block = \Phad\Blocks::FORM_SUBMIT;

        $args = $this->args;

        extract($args);
        ob_start();
        require($this->get_compiled_file_path());
        $output = ob_get_clean();

        // $this->output = $output;
        return $output;
    }

    /**
     * get a 2-dim array of js & css files associated with this view. that is same-named css & js files & css & js files in a same-named subdirectory.
     *
     * When is say 'same-named' i mean without the `.php` file extension
     *
     * @return `['js'=>[], 'css'=>[]]` where both the sub-arrays are numeric with file paths as values
     */
    public function resource_files(){
        $path = $this->path;
        $css_path = $path.'.css';
        $js_path = $path.'.js';

        $files = ['js'=>[], 'css'=>[]];
        if (file_exists($js_path)){
            $files['js'][] = $js_path;
        } 
        if (file_exists($css_path)){
            $files['css'][] = $css_path;
        }

        if (is_dir($path)){
            foreach (scandir($path) as $file){
                if (!is_file($path.'/'.$file))continue;
                if (substr($file,-4)=='.css')$files['css'][] = $path.'/'.$file;
                else if (substr($file,-3)=='.js')$files['js'][] = $path.'/'.$file;
            }
        }
        
        return $files;
    }
    /**
     * Get the item's html view
     *
     * @return html for your item
     */
    public function html(){
        $this->compileAsNeeded();
        if ($this->html !== false)return $this->html;
        $phad_block = \Phad\Blocks::VIEW;

        $args = $this->args;
        extract($args);
        ob_start();
        require($this->get_compiled_file_path());
        $output = ob_get_clean();

        $this->mode = null;
        $this->html = $output;
        return $output;
    }

    /**
     * Get an html view with no data items
     *
     * @return string html (or whatever your item's view returns)
     * @deprecated because it does the same as html()
     */
    public function html_with_no_data(){
        $this->compileAsNeeded();
        if ($this->html_with_no_data !== false)return $this->html_with_no_data;
        // $phad_mode = 'display_with_empty_object';
        $phad_block = \Phad\Blocks::VIEW;

        $args = $this->args;

        extract($args);
        ob_start();
        require($this->get_compiled_file_path());
        $output = ob_get_clean();

        $this->html_with_no_data = $output;
        return $output;
    }


    ///////////////////////////
    //
    /////// Utility & Compilation ////////
    //
    ///////////////////////////
    /**
     * Get the item template's source code
     */
    protected function source(){
        return $this->source
            ?? ($this->source=file_get_contents($this->templateFile))
            ;
    }

    /**
     * get path to compiled file
     */
    public function get_compiled_file_path(){
        $outFile = $this->path.'.compiled.php';
        return $outFile;
    }
    /**
     * Save content to the compiled file
     */
    public function putCompiledFile($content){
        $outFile = $this->get_compiled_file_path();
        file_put_contents($outFile, $content);
    }

    /**
     * Compile the item. 
     * @see compileAsNeeded() to conditionally compile only if the template has changed
     */
    public function compile(){
        $compiler = new \Phad\TemplateCompiler();

        try {
            $source = $this->source();
            if (strlen(trim($source))==null)throw new \Exception("Cannot compile '".$this->templateFile."' because it is empty");
            $src = $compiler->compile($source, file_get_contents(__DIR__.'/template/main.php'));
        } catch (\Error $e){
            $msg = 'Argument 1 passed to Taeluf\PHTML::insertCodeBefore() must be an instance of DOMNode, null given';
            if (strpos($e->getMessage(),$msg)!==false){
                throw new \Exception("\nThere was an error processing phad item '{$this->name}'. Maybe it has no html nodes? Idk.\n");
            }
            throw $e;
        }
        // $output = $compiler->output();
        // $src = $output['view'];

        $this->putCompiledFile($src);
    }
    /**
     * Compile only if the template has changed or there is no compiled item output. Will only compile once per instance of this class, regardless of changes on disk.
     *
     * @return true if compilation was performed. false otherwise.
     */
    public function compileAsNeeded(){
        if ($this->instanceHasBeenCompiled){
            return false;
        } else if (!file_exists($this->get_compiled_file_path())){
            $this->compile();
        } else if ($this->force_compile){
            $this->compile();
        } else if (filemtime($this->templateFile)>=filemtime($this->get_compiled_file_path())){
            $this->compile();
        } else return false;

        $this->instanceHasBeenCompiled = true;
        return true;

    }

    /**
     * Generate an sql CREATE TABLE statement from this item if it is a form
     *
     * @return string sql CREATE TABLE statement
     */
    public function create_table_statement(): string {
        $info = $this->info();
        if (!is_object($info)||!isset($info->properties)){
            $name = $this->name;
            throw new \Exception("Cannot load form info for '$name'. Either: It is not a phad item, You cannot access it, or It does not have properties.");
        }
        $properties = $info->properties;
        // print_r($properties);
        $table = strtolower($info->name);
        $statement = "CREATE TABLE `$table` (\n    ";
        $did_first = false;
        foreach ($properties as $name=>$details){
            $col_str = $this->col_str($name, $details);
            if ($did_first)$statement .=",\n    ";
            $statement .= $col_str;
            $did_first = true;
        }

        $statement .= "\n);";

        return $statement;
    }

    /**
     * Generate sql for create an individual column from node properties array
     *
     * @param $column_name the name of the column to make
     * @param $dom_input the array of attributes and info about the html input
     * @return a string like `name` VARCHAR(256)
     */
    function col_str(string $column_name, array $dom_input){
        $col = "`$column_name`";
        if ($dom_input['tagName']=='textarea')return "$col TEXT";


        if ($dom_input['tagName']=='select'){
            $type = "ENUM('"
                .implode("','", $dom_input['options'])
                ."')";
            return "$col $type";
        }

        if ($dom_input['tagName']!='input')throw new \Exception("Cannot handle tagName '".$dom_input['tagName']."'");

        $type = $dom_input['type'];
        if ($type=='checkbox')return "$col TINYINT";
        else if ($type=='hidden' && $column_name=='id')return "$col int unsigned PRIMARY KEY AUTO_INCREMENT";
        else if ($type=='text'){
            $len = 0;
            if (isset($dom_input['maxlength']))$len = (int)$dom_input['maxlength'];
            if ($len < 256)$len = 256;
            return "$col VARCHAR($len)";
        } else if ($type=='email'){
            return "$col VARCHAR(256)";
        } else if ($type=='phone'){
            return "$col VARCHAR(20)";
        } else if ($type=='radio'){
            return "$col VARCHAR(256)";
        } else if ($type=='number'){
            return "$col int";
        } else if ($type=='backend' && ($column_name=='id' ||  substr($column_name,-3)=='_id')){
            return "$col int unsigned";
        } else if ($type=='backend' && $column_name=='uuid'){
            return "$col binary(16) NOT NULL DEFAULT (UUID_TO_BIN( UUID() ) )";
        } else if ($type=='backend'){
            return "$col VARCHAR(256)";
        }


        throw new \Exception("Cannot handle input type '$type'");
    }


    /**
     * Output html. @see(html())
     */
    public function __toString(){
        return $this->html();
    }

}